forgot-password-form.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. 'use client';
  2. import { useState } from 'react';
  3. import { useRouter } from 'next/navigation';
  4. import { Button } from '@/components/ui/button';
  5. import { Input } from '@/components/ui/input';
  6. import { Label } from '@/components/ui/label';
  7. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
  8. import { AlertCircle, ArrowLeft, Mail } from 'lucide-react';
  9. import { Alert, AlertDescription } from '@/components/ui/alert';
  10. import { useToast } from '@/hooks/use-toast';
  11. import { useTranslations } from "next-intl";
  12. import Link from 'next/link';
  13. interface ForgotPasswordFormProps {
  14. locale: string;
  15. }
  16. export default function ForgotPasswordForm({ locale }: ForgotPasswordFormProps) {
  17. const [email, setEmail] = useState('');
  18. const [isLoading, setIsLoading] = useState(false);
  19. const [isSuccess, setIsSuccess] = useState(false);
  20. const [error, setError] = useState('');
  21. const { toast } = useToast();
  22. const router = useRouter();
  23. const t = useTranslations("auth.forgotPassword");
  24. const tErrors = useTranslations("auth.errors");
  25. const handleSubmit = async (e: React.FormEvent) => {
  26. e.preventDefault();
  27. setIsLoading(true);
  28. setError('');
  29. try {
  30. const response = await fetch('/api/auth/forgot-password', {
  31. method: 'POST',
  32. headers: {
  33. 'Content-Type': 'application/json',
  34. },
  35. body: JSON.stringify({
  36. email,
  37. locale,
  38. }),
  39. });
  40. const data = await response.json();
  41. if (response.ok) {
  42. setIsSuccess(true);
  43. toast({
  44. title: t('emailSent'),
  45. description: '请检查您的邮箱以重置密码。',
  46. });
  47. } else {
  48. setError(data.error || tErrors('networkError'));
  49. }
  50. } catch (error) {
  51. setError(tErrors('networkError'));
  52. } finally {
  53. setIsLoading(false);
  54. }
  55. };
  56. if (isSuccess) {
  57. return (
  58. <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800 px-4">
  59. <Card className="w-full max-w-md">
  60. <CardHeader className="space-y-1 text-center">
  61. <div className="flex justify-center mb-4">
  62. <div className="rounded-full bg-green-100 dark:bg-green-900 p-3">
  63. <Mail className="h-6 w-6 text-green-600 dark:text-green-400" />
  64. </div>
  65. </div>
  66. <CardTitle className="text-2xl font-bold">
  67. {t('emailSentTitle')}
  68. </CardTitle>
  69. <CardDescription>
  70. {t('emailSentDesc')}
  71. </CardDescription>
  72. </CardHeader>
  73. <CardContent className="space-y-4">
  74. <div className="text-center text-sm text-muted-foreground">
  75. <p>{t('checkEmailAt')}</p>
  76. <p className="font-medium text-foreground mt-1">{email}</p>
  77. </div>
  78. <div className="text-center text-sm text-muted-foreground">
  79. <p>{t('noEmailReceived')}</p>
  80. <Button
  81. variant="link"
  82. className="p-0 h-auto text-primary"
  83. onClick={() => setIsSuccess(false)}
  84. >
  85. {t('resendLink')}
  86. </Button>
  87. </div>
  88. <div className="text-center">
  89. <Link
  90. href={`/${locale}/auth/login`}
  91. className="inline-flex items-center text-sm text-primary hover:underline"
  92. >
  93. <ArrowLeft className="mr-2 h-4 w-4" />
  94. {t('backToLogin')}
  95. </Link>
  96. </div>
  97. </CardContent>
  98. </Card>
  99. </div>
  100. );
  101. }
  102. return (
  103. <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800 px-4">
  104. <Card className="w-full max-w-md">
  105. <CardHeader className="space-y-1">
  106. <CardTitle className="text-2xl font-bold text-center">
  107. {t('title')}
  108. </CardTitle>
  109. <CardDescription className="text-center">
  110. {t('subtitle')}
  111. </CardDescription>
  112. </CardHeader>
  113. <CardContent className="space-y-4">
  114. {error && (
  115. <Alert variant="destructive">
  116. <AlertCircle className="h-4 w-4" />
  117. <AlertDescription>{error}</AlertDescription>
  118. </Alert>
  119. )}
  120. <form onSubmit={handleSubmit} className="space-y-4">
  121. <div className="space-y-2">
  122. <Label htmlFor="email">{t('email')}</Label>
  123. <Input
  124. id="email"
  125. type="email"
  126. placeholder={t('emailPlaceholder')}
  127. value={email}
  128. onChange={(e) => setEmail(e.target.value)}
  129. required
  130. disabled={isLoading}
  131. />
  132. </div>
  133. <Button
  134. type="submit"
  135. className="w-full"
  136. disabled={isLoading}
  137. >
  138. {isLoading ? t('sending') : t('sendResetLink')}
  139. </Button>
  140. </form>
  141. <div className="text-center">
  142. <Link
  143. href={`/${locale}/auth/login`}
  144. className="inline-flex items-center text-sm text-primary hover:underline"
  145. >
  146. <ArrowLeft className="mr-2 h-4 w-4" />
  147. {t('backToLogin')}
  148. </Link>
  149. </div>
  150. {/* 隐私政策和用户协议 */}
  151. <div className="text-center text-xs text-muted-foreground space-y-1">
  152. <p>{t('agreeToTerms')}</p>
  153. <div className="flex justify-center space-x-4">
  154. <Link
  155. href={`/${locale}/privacy`}
  156. className="text-primary hover:underline"
  157. >
  158. {t('privacyPolicy')}
  159. </Link>
  160. <span>•</span>
  161. <Link
  162. href={`/${locale}/terms`}
  163. className="text-primary hover:underline"
  164. >
  165. {t('termsOfService')}
  166. </Link>
  167. </div>
  168. </div>
  169. </CardContent>
  170. </Card>
  171. </div>
  172. );
  173. }